/**
 * Copyright 2012-2013 Three Crickets LLC.
 * <p>
 * The contents of this file are subject to the terms of the Apache License
 * version 2.0: http://www.opensource.org/licenses/apache2.0.php
 * <p>
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly from Three Crickets
 * at http://threecrickets.com/
 */

package com.orientechnologies.orient.rhino;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;

import com.orientechnologies.orient.core.record.impl.ODocument;
import com.threecrickets.rhino.util.NativeRhinoUtil;

/**
 * Conversion between native Rhino objects and OrientDB-compatible types.
 * <p>
 * This class can be used directly in Rhino.
 * 
 * @author Tal Liron
 */
public class OrientTypesImplementation
{
	//
	// Operations
	//

	/**
	 * Recursively convert from native JavaScript to OrientDB-compatible types.
	 * <p>
	 * Recognizes JavaScript objects, arrays, Date objects and primitives.
	 * <p>
	 * Also recognizes JavaScript objects adhering to OrientDB's extended JSON,
	 * converting them to OrientDB types: {$rid:'recordid'}, {$binary:'base64'}.
	 * <p>
	 * Note that the {$date:timestamp} extended JSON formats are recognized as
	 * well as native JavaScript Date objects.
	 * 
	 * @param object
	 *        A Rhino native object
	 * @return An OrientDB-compatible object
	 */
	public Object to( Object object )
	{
		if( object instanceof NativeJavaObject )
		{
			// This happens either because the developer purposely creates a
			// Java object, or because it was returned from a Java call and
			// wrapped by Rhino.

			return ( (NativeJavaObject) object ).unwrap();
		}
		else if( object instanceof NativeArray )
		{
			// Convert Rhino array to list

			NativeArray array = (NativeArray) object;
			int length = (int) array.getLength();
			ArrayList<Object> list = new ArrayList<Object>( length );

			for( int i = 0; i < length; i++ )
				list.add( to( ScriptableObject.getProperty( array, i ) ) );

			return list;
		}
		else if( object instanceof ScriptableObject )
		{
			ScriptableObject scriptable = (ScriptableObject) object;

			// Is it in extended JSON format?
			Object r = orientJsonExtender.from( scriptable, false );
			if( r != null )
				return r;

			r = NativeRhinoUtil.from( scriptable );
			if( r != null )
				return r;

			// Convert regular Rhino object

			HashMap<String, Object> map = new HashMap<String, Object>();

			Object[] ids = scriptable.getAllIds();
			for( Object id : ids )
			{
				String key = id.toString();
				Object value = to( ScriptableObject.getProperty( scriptable, key ) );
				map.put( key, value );
			}

			return map;
		}
		else if( object instanceof Undefined )
		{
			return null;
		}
		else
		{
			return object;
		}
	}

	/**
	 * Recursively convert from OrientDB to native JavaScript values.
	 * <p>
	 * Converts to JavaScript objects, arrays, Date objects and primitives. The
	 * result is OrientDB-compatible.
	 * <p>
	 * Note that special OrientDB types (ORecordId, ORecordBytes) are not
	 * converted, but {@link OrientJsonExtender#to(Object,boolean,boolean)}
	 * recognizes them, so they can still be considered JSON-compatible in this
	 * limited sense.
	 * 
	 * @param object
	 *        An OrientDB-compatible object
	 * @return A JSON-compatible Rhino object
	 */
	public Object from( Object object )
	{
		return from( object, false );
	}

	/**
	 * Recursively convert from OrientDB to native JavaScript values.
	 * <p>
	 * Converts to JavaScript objects, arrays, Date objects and primitives. The
	 * result is OrientDB-compatible.
	 * <p>
	 * Note that special OrientDB types (ORecordId, ORecordBytes) are not
	 * converted, but {@link OrientJsonExtender#to(Object,boolean,boolean)}
	 * recognizes them, so they can still be considered JSON-compatible in this
	 * limited sense.
	 * 
	 * @param object
	 *        An OrientDB-compatible object
	 * @param extendedJSON
	 *        Whether to convert extended JSON objects
	 * @return A JSON-compatible Rhino object
	 */
	public Object from( Object object, boolean extendedJSON )
	{
		if( object instanceof List<?> )
		{
			// Convert list to NativeArray

			List<?> list = (List<?>) object;
			Scriptable array = NativeRhinoUtil.newArray( list.size() );

			int index = 0;
			for( Object item : list )
				ScriptableObject.putProperty( array, index++, from( item, extendedJSON ) );

			return array;
		}
		else if( object instanceof ODocument )
		{
			// Convert ODocument to NativeObject

			ODocument document = (ODocument) object;
			Scriptable nativeObject = NativeRhinoUtil.newObject();

			/*
			 * for( Map.Entry<String, Object> entry : document ) { Object value
			 * = from( entry.getValue(), extendedJSON );
			 * ScriptableObject.putProperty( nativeObject, entry.getKey(), value
			 * ); }
			 */

			for( String fieldName : document.fieldNames() )
			{
				Object value = from( document.field( fieldName ), extendedJSON );
				ScriptableObject.putProperty( nativeObject, fieldName, value );
			}

			return nativeObject;
		}
		else if( object instanceof Date )
		{
			return NativeRhinoUtil.to( (Date) object );
		}
		else if( object instanceof Long )
		{
			// Wrap Long so to avoid conversion into a NativeNumber (which would
			// risk losing precision!)

			return NativeRhinoUtil.wrap( (Long) object );
		}
		else
		{
			if( extendedJSON )
			{
				Object r = orientJsonExtender.to( object, true, false );
				if( r != null )
					return r;
			}

			return object;
		}
	}

	// //////////////////////////////////////////////////////////////////////////
	// Private

	private final OrientJsonExtender orientJsonExtender = new OrientJsonExtender();
}
